昨天完成了登入後票倦的 layout。今天目標是建立登入頁面的 layout。
因為使用的 ReactNative,所基本上都是要使用 component 的處理方式。為了能夠方便搭建大的元件,會從一些功能型元件,比如說自製的對齊元件 Stack,或是自行包裝的 Text 等等元件來作處理。由小而大的搭建元件。
這個登入頁面,可以分成以下幾種屬性:
在今天的實做中,先以沒有狀態的元件作處理。然後在透過一些條件式,來組成可以有狀態的元件。
import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { PropsWithChildren } from 'react';
import { ViewProps, View } from 'react-native';
export interface StackProps extends PropsWithChildren, ShortcutProps, ViewProps {
flex?: number
direction?: 'row'|'column'
gap?: number
alignItems?: 'flex-start'|'flex-end'|'center'|'stretch'|'baseline'
justifyContent?: 'flex-start'|'flex-end'|'center'|'space-between'|'space-around'|'space-evenly'
}
export function Stack({
flex,
direction,
gap,
alignItems,
justifyContent,
children,
style,
...restProps
}: StackProps) {
return (
<View style={[defaultShortcuts(restProps), {
flex,
flexDirection: direction,
gap,
alignItems,
justifyContent,
},style]} {...restProps} >
{children}
</View>
)
}
這個 Stack 單純就是用來作對齊的元件。
VStack 是用來作垂直對齊的 View,可以使用 Stack 來建構如下
import { Stack, StackProps } from './Stack';
interface VStackProps extends StackProps {}
export function VStack(props: VStackProps) {
return (
<Stack {...props} direction='column'/>
)
}
HStack 是用來作水平對齊的 View,可以使用 Stack 來建構如下
import { Stack, StackProps } from './Stack';
interface HStackProps extends StackProps {}
export function HStack(props: HStackProps) {
return (
<Stack {...props} direction='row'/>
)
}
import { HStack } from '@/components/HStack';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { VStack } from '@/components/VStack';
import { KeyboardAvoidingView, ScrollView } from 'react-native';
export default function Login() {
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{flex: 1}}>
<VStack flex={1} justifyContent='center' alignItems='center' p={40} gap={40}>
<HStack gap={10}>
<Text fontSize={30} bold mb={20}>Ticket Booking</Text>
<TabBarIcon name='ticket' size={50}/>
</HStack>
</VStack>
</ScrollView>
</KeyboardAvoidingView>
);
}
import { Redirect, Stack } from 'expo-router';
export default function AppLayout() {
// check from context if user is logged in
const isLogggedIn = false;
if (!isLogggedIn) {
return <Redirect href="/login" />
}
return <Stack screenOptions={{ headerShown: false }} />
}
當下結果就會如下:
實做 TextView
import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { PropsWithChildren } from 'react';
import { TextProps, Text as RNText } from 'react-native';
interface CustomTextProps extends PropsWithChildren, ShortcutProps, TextProps {
fontSize?: number;
bold?: boolean;
underline?: boolean;
color?: string;
}
export function Text({
fontSize = 18,
bold,
underline,
color,
children,
style,
...restProps
}: CustomTextProps) {
return (
<RNText style={[defaultShortcuts(restProps), {
fontSize,
fontWeight: bold? 'bold': 'normal',
textDecorationLine: underline? 'underline': 'none',
color,
}, style]}
{...restProps}
>
{children}
</RNText>
)
}
透過客制化的 TextView 去新增屬性,並且加入縮寫的轉換元件
InputView 實做如下
import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { TextInput, TextInputProps } from 'react-native';
interface InputProps extends ShortcutProps, TextInputProps {
}
export function Input(props: InputProps) {
return (
<TextInput
style={[defaultShortcuts(props), {
fontSize: 16,
borderRadius: 16,
backgroundColor: 'lightgray',
color: 'black'
}]}
{...props}
/>
)
}
這個 InputView 加入了客製化的 ShortcutProps ,讓開發人員可以透過簡寫的方式使用,並且設定好預設的樣式。
在 login.tsx 加入 email 與 password 欄位,如下:
<VStack w={'100%'} gap={30}>
<VStack gap={5}>
<Text ml={10} fontSize={14} color='gray'>Email</Text>
<Input
value={email}
onChangeText={setEmail}
placeholder='Email'
placeholderTextColor='darkgray'
autoCapitalize='none'
autoCorrect={false}
h={48}
p={14}
/>
</VStack>
<VStack gap={5}>
<Text ml={10} fontSize={14} color='gray'>Password</Text>
<Input
secureTextEntry
value={password}
onChangeText={setPassword}
placeholder='Password'
placeholderTextColor='darkgray'
autoCapitalize='none'
autoCorrect={false}
h={48}
p={14}
/>
</VStack>
</VStack>
並且加入 react hook 來控制狀態,使用 useState 來處理。如下
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
結果如下:
基本 layout:
輸入後如下
import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { ActivityIndicator, StyleSheet, TouchableOpacity, TouchableOpacityProps } from 'react-native';
import { Text } from './Text';
interface ButtonProps extends ShortcutProps, TouchableOpacityProps {
variant?:'contained'|'outlined'|'ghost',
isLoading?: boolean,
}
export function Button({
onPress,
children,
variant = 'contained',
isLoading,
...restProp
}: ButtonProps) {
return (
<TouchableOpacity
disabled={isLoading}
onPress={onPress}
style={[
defaultShortcuts(restProp),
styles[variant].button,
isLoading && disabled.button
]}
{...restProp}
>
{isLoading?
<ActivityIndicator animating size={22}/>:
<Text style={[styles[variant].text]}>{children}</Text>
}
</TouchableOpacity>
)
}
const styles = {
contained: StyleSheet.create({
button: {
padding: 14,
borderRadius: 50,
backgroundColor: 'black'
},
text: {
textAlign: 'center',
color: 'white',
fontSize: 18,
}
}),
outlined: StyleSheet.create({
button: {
padding: 14,
borderRadius: 50,
borderColor: 'darkgray',
borderWidth: 1,
},
text: {
textAlign: 'center',
color: 'black',
fontSize: 18,
}
}),
ghost: StyleSheet.create({
button: {
padding: 14,
borderRadius: 50,
backgroundColor: 'transparent'
},
text: {
textAlign: 'center',
color: 'black',
fontSize: 18,
}
})
}
const disabled = StyleSheet.create({
button: {
opacity: 0.5
}
});
上面的寫法,是把 Button 包裝成樣式元件,樣式會根據傳入的狀態而變更。
import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { View } from 'react-native';
interface DividerProps extends ShortcutProps {}
export function Divider(props: DividerProps) {
return (
<View
style={[
defaultShortcuts(props),
{
backgroundColor: 'lightgray',
height: 1
},
]}
/>
)
}
這個元件只是單一的分隔線,用簡單的 View 即可。
在 login.tsx 加入以下元件
<Button
w={'100%'}
isLoading={false} //TODO: finished once we have authenticated provider
onPress={() =>{ }} //TODO: finished once we have authenticated provider
>
{authMode === 'login'? 'Login': 'Register'}
</Button>
<Divider w={'90%'}/>
<Text onPress={onToggleAuthMode} fontSize={16} underline>
{authMode === 'login'? 'Register new account': 'Login to account'}
</Text>
並且設定以下控制函數
const [authMode, setAuthMode] = useState<'login'|'register'>('login');
function onToggleAuthMode() {
setAuthMode(authMode === 'login'? 'register':'login');
}
結果如下:
使用者可以透過分隔線下方的連結切換目前的是要作登入還是註冊。也就是點完連結登入就變成註冊如下圖:
到這邊,基本的登入頁面就大致完成。而控制登入的邏輯,在接下來的文章會繼續進行。
經過這兩天的開發,可以發現手機應用程式與一般後端開發程式不同之處。一個元件同時包含著畫面顯示的宣告邏輯與事件觸發的處理邏輯。即便透過像是 react hook 這類方法最後還是會被匯集到某個 render 元件。這樣的邏輯混合 ui 的狀況比單純 api 的處理複雜許多。
另外是,手機端程式對於每個顯示的頁面 Activity 會有其生命週期。載入某個 Activity 的小元件 Fragement 也會依附於 Activity 生命週期,有屬於他自己的元件週期。這些都是當需要開發手機程式時所需要考量的。
總結是,不論是作哪種系統的開發工作。都一定要先經過設計,否則會造成一些重工或是做出一些增加認知負擔的元件。